#!/usr/bin/env python
"""
CTPRPCServer

Server implementation of the CTP-RPC v2 protocol.

Steps for using server object:
	1. Create the server instance
		serv = CTPRPCServer ( ( "", 8000 ) [, "ctppasswd"] )
	
	   the first parameter specifies the listen address and port,
	   the second (optional) parameter specifies the file that holds users and passwords

	2. Register some methods to publish. This can be done in one of three ways:
		a. Using register_function ( func [, func_name ] )
		   This method will publish a single function
		   
		b. Using register_instance ( inst )
		   This method will publish all methods in the instance specified.
		
		c. Using register_class ( klass [, init_params] [, init_params_dict] )
		   This method will create an instance of class for each session opened with server
		   and publish the methods of that instance.
		   The 'init_params' and 'init_params_dict' optional arguments are passed to the class constructor
		   as positional and named arguments respectively.

		NOTES for (b) and (c):
			* Only instance methods that don't start with '_' will be published; if you intend to publish
			  other methods you have to implement a method called listMethods() which
			  returns the list of methods to publish.

			* The attribute '_ctp_connection' is automatically added to the registered instance.
			  It can be used to access the following connection information:
			  
			  - encrypt_alg_id	id of the encryption algorithm negotiated with client
			  - comrpess_alg_id	id of the comrpession algorithm negotiated with client
			  - client_address	( ip_address, port ) tuple of the client
			  - server_address	( ip_address, port ) tuple of the server
			  - client_language	the language platform of the client (e.g.: "python", "dotnet", ...)
			  - login		username used for authentication

			  - _encrypt		this flag is set when the request packet is encrypted
			  			if you leave this flag untouched the response will be encrypted too
			  - _compress		this flag is set when the request packet is compressed
			  			if you leave this flag untouched the response will be compressed too
			  - _compress_data	if this flag is set and the request packet has some extra-data (usually files) then the extra-data is compressed;
			  			if you leave this flag untouched and the response has some extra-data then the extra-data will be compressed too

			  - close()		call this method to close the CTP connection
	
	3. Register some events to publish
		serv.register_event ( "config_changed" )
		serv.register_event ( "job_completed" )
	
	4. Serve incoming connections:
		serv.serve_forever()
		
"""

import sys, os, os.path
import socket
import os3.utils.thread_control
import cPickle
import traceback
try:
	from hashlib import md5
	from hashlib import sha1 as sha
except:
	import md5
	import sha

import hmac

from CTPRPCCommon import *


# ***************************************************************
# CTPRPCConnection class
class CTPRPCConnection ( object ):
	"""
	Questa classe viene creata per ogni connessione in ingresso dal CTPRPCServer principale.
	"""
		
	def __init__ ( self, tc, thread_name, client_sock, methods, events, instance, server_rsa_key, ctppasswd, serialize_calls, socket_timeout ):
		self._tc = tc
		self._thread_name = thread_name
		self._sock = client_sock
		self._methods = methods.copy()
		self._events = events
		self._instance = instance
		self._server_rsa_key = server_rsa_key
		self._handshake_complete = 0
		self._ctppasswd = ctppasswd
		self.login = ''

		self.encrypt_alg_id = 0
		self.compress_alg_id = 0
		self.session_id = id(self)
		self.client_language = ""

		self._seq_num = 0
		self._encrypt = 1
		self._compress = 1
		self._compress_data = 1
		self._serialize_calls = serialize_calls
		self._socket_timeout = socket_timeout
		
		self.client_address = self._sock.getpeername()
		self.server_address = self._sock.getsockname()

		debug.write ( 1, "[%d] CTPRPCConnection created" % self.session_id )


	# *******************************************************
	# close() - chiude la connessione CTP-RPC
	def close ( self ):
		"""
		close ( self )

		Chiude la connession CTP-RPC.
		"""

		#unregister events
		debug.write ( 2, "[%d] unregistering events for this connection" % self.session_id )
		self._tc.lock_acquire ( "events_lock" )
		try:
			for v in self._events.itervalues():
				while self in v: v.remove ( self )
		finally:
			self._tc.lock_release ( "events_lock" )
		
		self._tc.kill ( self._thread_name )

		debug.write ( 1, "[%d] CTPRPCConnection closed" % self.session_id )

		
	# *******************************************************
	# handle() - gestisce una nuova connessione TCP
	def handle ( self ):
		"""
		handle ( self )

		Gestisce una nuova connessione CTP-RPC.
		"""

		debug.write ( 2, "[%d] handle() called" % self.session_id )

		#setto il timeout
		self._sock.settimeout ( self._socket_timeout )		# 5 minutes by default

		#send welcome message
		send_welcome ( self._sock )

		if not self._handshake(): return

		if not self._authenticate(): return

		commands = {
				CMD_HEARTBEAT:		self._cmd_heartbeat,
				CMD_INVOKE:		self._cmd_invoke,
				CMD_SUBSCRIBE:		self._cmd_subscribe,
				CMD_UNSUBSCRIBE:	self._cmd_unsubscribe,
			}

		#processa i comandi che arrivano
		while self._tc.keep_run ( self._thread_name ):
			
			debug.write ( 2, "[%d] handle(): waiting for command..." % self.session_id )
			
			ret = self._get_command()
			if not ret:
				#drop connection
				debug.error ( "[%d] handle(): error reading command, exiting" % self.session_id )
				break

			cmd, args = ret
			if not cmd in commands.keys():
				#send error
				debug.error ( "[%d] handle(): bad command received, sending RESP_BADCOMMAND (received command: %d)" % ( self.session_id, cmd ) )
				self._send_response ( RESP_BADCOMMAND )
				continue

			debug.write ( 2, "[%d] handle(): received command %d, processing it..." % ( self.session_id, cmd ) )

			cmd_handler = commands[cmd]
			if not cmd_handler ( args ):
				#drop connection
				debug.error ( "[%d] handle(): command handler for command %d failed, exiting" % ( self.session_id, cmd ) )
				break

		debug.write ( 2, "[%d] handle(): exiting" % self.session_id )


	# *******************************************************
	# fire_event() - genera un evento
	def fire_event ( self, event_name, *args, **kwargs ):
		"""
		fire_event ( self, event_name, *args, **kwargs )

		Genera un evento verso il client.
		"""

		debug.write ( 2, "[%d] fire_event() called, event_name=%s" % ( self.session_id, event_name ) )

		self._tc.lock_acquire ( "resp_lock" )
		try:
			compress_mod = None
			encrypt_key = None
			hmac_key = 'CTP-RPC'
			block_size = 1

			if self._handshake_complete:
				if self._compress: compress_mod = self._compress_alg
				if self._encrypt and self._encrypt_alg:
					encrypt_key = self._client_sess_key
					block_size = self._encrypt_alg.block_size
				if is_encryption_supported: hmac_key = self._hmac_key


			debug.write ( 3, "[%d] sending SERV_NOTIFY packet" % self.session_id )
			debug.write ( 3, "[%d]  event_name: %s" % ( self.session_id, event_name ) )
			debug.write ( 3, "[%d]  args length: %s" % ( self.session_id, len(args) ) )
			debug.write ( 3, "[%d]  kwargs length: %s" % ( self.session_id, len(kwargs) ) )
			debug.write ( 3, "[%d]  seq_num: 0" % self.session_id )
		
			send_object ( self._sock, ( SERV_NOTIFY, ( event_name, args, kwargs ), 0 ), self.session_id,
				hmac_key, compress_mod, encrypt_key, block_size, self._compress_data, self._relax_serialize )

		finally:
			self._tc.lock_release ( "resp_lock" )


	# *******************************************************
	# _resolve_dotted_attribute() - risolve un nome di metodo 'puntato'
	def _resolve_dotted_attribute ( self, obj, attr ):
		"""
		resolve_dotted_attribute ( a, 'b.c.d' ) => a.b.c.d

		Resolves a dotted attribute name to an object.  Raises
		an AttributeError if any attribute in the chain starts with a '_'.
		"""
	
		attrs = attr.split ( '.' )
	
		for i in attrs:
			if i.startswith ( '_' ):
				raise AttributeError ( "attempt to access private attribute '%s'" % i )
			else:
				obj = getattr ( obj, i, None )
				if not obj: return None
	
		return obj
	

	# *******************************************************
	# _cmd_heartbeat() - risponde con OK
	def _cmd_heartbeat ( self, args ):
		"""
		_cmd_heartbeat ( self, args ) -> bool

		Gestisce il comando CMD_HEARTBEAT: in pratica non fa nulla e risponde sempre RESP_OK.
		Viene usato come keep-alive per mantenere la connessione attiva,
		se il client non spedisce nulla per 5 minuti il server droppa la connessione.

		Il parametro ``args`` viene ignorato.
		"""
		
		self._send_response ( RESP_OK )
		return 1

	
	# *******************************************************
	# _cmd_invoke() - invoca un metodo della lista dei metodi
	def _cmd_invoke ( self, args ):
		"""
		_cmd_invoke ( self, args ) -> bool

		Gestisce il comand CMD_INVOKE uasto per invocare un metodo registrato sul server.
		I metodo vengono registrati con le funzioni register_XXX().

		Il parametro ``args`` e' una tupla contenente:
		- nome del metodo da invocare
		- lista di parametri posizionali
		- dizionario di parametri con nome

		La risposta puo' essere una tra le seguenti:
		- RESP_EXCEPTION se si e' verificata un'eccezione durante l'esecuzione del metodo;
		  in questo caso gli argomenti della risposta saranno:
		  - classe o istanza di eccezione
		  - istanza o parametro di eccezione
		  - lista di linee di traceback
		- RESP_BADMETHOD se il metodo invocato non esiste tra i metodi registrati
		- RESP_OK se e' andato tutto bene; gli argomenti di risposta conterranno solo il valore
		  di ritorno del metodo
		"""

		if len ( args ) != 3: return 0

		method, pos_args, named_args = args
		if not pos_args: pos_args = []
		if not named_args: named_args = {}
		
		if not isinstance ( method, str ) or not isinstance ( pos_args, list ) or not isinstance ( named_args, dict ):
			return 0

		func = None
		
		if self._methods.has_key ( method ):
			func = self._methods[method]
		elif self._instance:
			func = self._resolve_dotted_attribute ( self._instance, method )

		if not func:
			self._send_response ( RESP_BADMETHOD )
			return 1

		try:
			try:
				if self._serialize_calls: self._tc.lock_acquire ( "method_invoke" )
				ret = func ( *pos_args, **named_args )
			finally:
				if self._serialize_calls: self._tc.lock_release ( "method_invoke" )
		except:
			exc = sys.exc_info()
			tb = traceback.extract_tb ( exc[2] )
			
			if self.client_language == "python":
				args = ( exc[0], exc[1], tb )
			else:
				args = ( exc[0].__name__, str(exc[1]), tb )

			self._send_response ( RESP_EXCEPTION, args )
			
			del exc		#mmm... avoid circular references...
			del tb
			del args

			return 1
		
		self._send_response ( RESP_OK, ( ret, ) )
		return 1


	# *******************************************************
	# _cmd_subscribe() - sottoscrizione di un evento
	def _cmd_subscribe ( self, args ):
		"""
		_cmd_subscribe ( self, args ) -> bool

		Gestisce il comando CMD_SUBSCRIBE: registra questo client nell'elenco di gestione per l'evento specificato.

		Il parametro ``args`` contiene solo il nome dell'evento da sottoscrivere.
		"""

		if len ( args ) != 1: return 0
		if not isinstance ( args[0], str ): return 0

		event_name = args[0]

		self._tc.lock_acquire ( "events_lock" )
		try:
			if event_name not in self._events.keys():
				self._send_response ( RESP_BADEVENT, () )
			else:
				self._events [ event_name ].append ( self )
		finally:
			self._tc.lock_release ( "events_lock" )

		self._send_response ( RESP_OK, () )
		return 1


	# *******************************************************
	# _cmd_unsubscribe() - de-sottoscrizione di un evento
	def _cmd_unsubscribe ( self, args ):
		"""
		_cmd_subscribe ( self, args ) -> bool

		Gestisce il comando CMD_UNSUBSCRIBE: de-registra questo client dall'elenco di gestione per l'evento specificato.

		Il parametro ``args`` contiene solo il nome dell'evento da de-registrare.
		"""

		if len ( args ) != 1: return 0
		if not isinstance ( args[0], list ): return 0

		event_name = args[0]

		self._tc.lock_acquire ( "events_lock" )
		try:
			if event_name not in self._events.keys():
				self._send_response ( RESP_BADEVENT, () )
			else:
				self._events [ event_name ].remove ( self )
		finally:
			self._tc.lock_release ( "events_lock" )

		self._send_response ( RESP_OK, () )
		return 1


	# *******************************************************
	# _get_command() - legge un nuovo comando dal socket
	def _get_command ( self ):
		"""
		_get_command ( self ) -> ( int, tuple )

		Legge dal socket un nuovo comando e restituisce una tupla con
		l'id del comando e una tupla di argomenti.
		"""
		
		compress_mod = None
		encrypt_key = None
		hmac_key = 'CTP-RPC'

		if self._handshake_complete:
			compress_mod = self._compress_alg
			if self._encrypt_alg: encrypt_key = self._server_sess_key
			if is_encryption_supported: hmac_key = self._hmac_key

		ret = read_object ( self._sock, self.session_id, hmac_key, compress_mod, encrypt_key )
		if not ret: return None

		obj, self._encrypt, self._compress, self._compress_data = ret
		if not isinstance ( obj, list ) or len ( obj ) != 3: return None

		cmd, args, seq_num = obj
		if not isinstance ( cmd, int ) or not isinstance ( args, list ): return None

		if seq_num != self._seq_num: return None
		self._seq_num += 1
		if self._seq_num > 9999999: self._seq_num = 1

		return cmd, args


	# *******************************************************
	# _send_response() - invia una risposta al client
	def _send_response ( self, resp, args = None ):
		"""
		_send_response ( self, resp, args )

		Invia sul socket una risposta ad un comando con la relativa tupla di argomenti.
		"""

		self._tc.lock_acquire ( "resp_lock" )
		try:
			compress_mod = None
			encrypt_key = None
			hmac_key = 'CTP-RPC'
			block_size = 1

			if self._handshake_complete:
				if self._compress: compress_mod = self._compress_alg
				if self._encrypt and self._encrypt_alg:
					encrypt_key = self._client_sess_key
					block_size = self._encrypt_alg.block_size
				if is_encryption_supported: hmac_key = self._hmac_key
		
			if not args: args = ()
			send_object ( self._sock, ( resp, args, self._seq_num ), self.session_id,
				hmac_key, compress_mod, encrypt_key, block_size, self._compress_data, self._relax_serialize )

			self._seq_num += 1
			if self._seq_num > 9999999: self._seq_num = 1
			
		finally:
			self._tc.lock_release ( "resp_lock" )

		
	# *******************************************************
	# _handshake() - effettua lo scambio di dati inziale per
	#                decidere l'encryption e la compression
	def _handshake ( self ):
		"""
		_handshake ( self ) -> bool

		Effettua lo scambio di dati iniziale per concordare gli algoritmi di cifratura e compressione,
		le chiavi di cifrature, la chiave di MAC, i vettori di inizializzazione degli algoritmi
		di cifratura e i vettori dei semi random del client e del server.

		Brevemente quello che accade:
		Viene ricevuto un CMD_HANDSHAKE_1 con i seguenti argomenti:
		- linguaggio (es.: "python")
		- algoritmi di crittografia supportati dal client: tupla di ID in ordine di preferenza
		- algoritmi di compressione supportati dal client: tupla di ID in ordine di preferenza
		- (OPZIONALE) client random vector: solo se il client supporta la crittografia
		- (OPZIONALE) chiave pubblica del client: tupla di ( modulo, esponente pubblico )
		  solo se il client supporta la crittografia

		Il server risponde con un RESP_HANDSHAKE_1 con i seguenti argomenti:
		- linguaggio (es.: "python")
		- ID dell'algoritmo di crittografia scelto
		- ID dell'algoritmo di compressione scelto
		- ID di sessione
		- (OPZIONALE) server random vector: solo se il server supporta la crittografia
		- (OPZIONALE) chiave pubblica del server: tupla di ( modulo, esponente pubblico )
		  solo se il server supporta la crittografia
		- (OPZIONALE) server pre-encryption key criptata con la chiave pubblica del client
		  solo se il server supporta la crittografia

		Se il client o il server non supportano la crittografia l'handshake si ferma qui.
		Altrimenti si prosegue e viene ricevuto un CMD_HANDSHAKE_2 con i seguenti argomenti:
		- client pre-encryption key criptata con la chiave pubblica del server

		Il server a questo punto ha tutti i dati per calcolare le seguenti informazioni:
		- client initialization vector
		- server initialization vector
		- chiave MAC
		- client encryption key
		- server encryption key

		Il server risponde con un RESP_HANDSHAKE_2 con i seguenti argomenti:
		- HMAC_MD5 ( hmac_key, client_pre_key + server_pre_key ) + HMAC_SHA ( hmac_key, server_pre_key + client_pre_key )
		
		Se e' tutto ok, l'handshake e' finito.
		"""

		debug.write ( 2, "[%d] entering _handshake(), receiving command..." % self.session_id )

		ret = self._get_command()
		if not ret:
			debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_1" % self.session_id )
			return 0

		cmd, params = ret

		if cmd != CMD_HANDSHAKE_1:
			debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_1, got command: %d" % ( self.session_id, cmd ) )
			return 0
		if len ( params ) < 3:
			debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_1, received params len: %d (expected at least: 3)" % ( self.session_id,
				len ( params ) ) )
			return 0

		if not isinstance ( params[0], str ) or not isinstance ( params[1], list ) or not isinstance ( params[2], list ):
			debug.error ( ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_1:\n" +
					"       param0 is %s (expected 'str')\n" +
					"       param1 is %s (expected 'list')\n" +
					"       param2 is %s (expected 'list')" ) %
					( self.session_id, type(params[0]).__name__, type(params[1]).__name__, type(params[2]).__name__ )
				)
			return 0

		self.client_language, client_supported_encryption, client_supported_compression = params[:3]
		self._relax_serialize = (self.client_language == 'python')

		debug.write ( 3, "[%d] _handshake(), got command CMD_HANDSHAKE_1:" % self.session_id )
		debug.write ( 3, "[%d]     client_language: %s" % ( self.session_id, self.client_language ) )
		debug.write ( 3, "[%d]     client_supported_encryption: %s" % ( self.session_id, client_supported_encryption ) )
		debug.write ( 3, "[%d]     client_supported_compression: %s" % ( self.session_id, client_supported_compression ) )

		#now choose the best encryption/compression algorithm
		self.encrypt_alg_id = 0
		for encr in client_supported_encryption:
			if encr in supported_encryption_types.keys():
				self.encrypt_alg_id = encr
				self._encrypt_alg = supported_encryption_types[encr][1]
				break
		
		self.compress_alg_id = 0
		for comp in client_supported_compression:
			if comp in supported_compression_types.keys():
				self.compress_alg_id = comp
				self._compress_alg = supported_compression_types[comp][1]
				break

		debug.write ( 3, "[%d] _handshake(), sending response RESP_HANDSHAKE_1:" % self.session_id )

		#build response
		resp = [ "python" ]			# language
		resp.append ( self.encrypt_alg_id )	# choosen encryption algorithm
		resp.append ( self.compress_alg_id )	# choosen compression algorithm
		resp.append ( self.session_id )		# session ID

		debug.write ( 3, "[%d]     client language: python" % self.session_id )
		debug.write ( 3, "[%d]     encrypt_alg_id: %s" % ( self.session_id, self.encrypt_alg_id ) )
		debug.write ( 3, "[%d]     compress_alg_id: %s" % ( self.session_id, self.compress_alg_id ) )
		debug.write ( 3, "[%d]     session_id: %s" % ( self.session_id, self.session_id ) )

		if is_encryption_supported and len ( client_supported_encryption ) and len ( params ) == 5:

			debug.write ( 2, "[%d] _handshake(): encryption is supported and params length == 5, processing encryption data..." % self.session_id )

			if not isinstance ( params[3], str ) or not isinstance ( params[4], list ):
				debug.error ( ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_1:\n" +
					"       param3 is %s (expected 'str')\n" +
					"       param4 is %s (expected 'list')" ) %
					( self.session_id, type(params[3]).__name__, type(params[4]).__name__ )
				)
				return 0

			self._client_random, client_pub_key = params[3:]

			debug.write ( 3, "[%d]     client_random: %s" % ( self.session_id, debug.tohex ( self._client_random ) ) )

			if len ( self._client_random ) != 32 or len ( client_pub_key ) != 2:
				debug.error ( ( "[%d] _handshake(): error in CMD_HANDSHAKE_1: client_random length: %d (expected 32), " +
				"client_pub_key length: %d (expected 2)" ) % ( self.session_id, len ( self._client_random ), len ( client_pub_key ) ) )
				return 0

			client_pub_key [ 1 ] = long ( client_pub_key [ 1 ] )
			self._client_rsa_key = RSA.construct ( client_pub_key )

			debug.write ( 3, "[%d]     client_rsa_key modulus: %s" % ( self.session_id, debug.tohex ( self._client_rsa_key.n ) ) )
			debug.write ( 3, "[%d]     client_rsa_key exponent: %s" % ( self.session_id, debug.tohex ( long ( self._client_rsa_key.e ) ) ) )


			#generate my data (for response)
			RANDPOOL.stir()
			self._server_random = RANDPOOL.get_bytes ( 32 )
			resp.append ( self._server_random )			# server random

			resp.append ( ( self._server_rsa_key.n, long ( self._server_rsa_key.e ) ) )	# server public key

			debug.write ( 3, "[%d]     server_random: %s" % ( self.session_id, debug.tohex ( self._server_random ) ) )
			debug.write ( 3, "[%d]     server_rsa_key modulus: %s" % ( self.session_id, debug.tohex ( self._server_rsa_key.n ) ) )
			debug.write ( 3, "[%d]     server_rsa_key exponent: %s" % ( self.session_id, debug.tohex ( long ( self._server_rsa_key.e ) ) ) )

			if self._encrypt_alg:
				server_pre_key = RANDPOOL.get_bytes ( self._encrypt_alg.key_size )
				block02 = build_pkcs_block_02 ( server_pre_key, RSA_KEY_LENGTH )
				enc = self._client_rsa_key.encrypt ( block02, 0 )[0]
				resp.append ( enc )

				debug.write ( 3, "[%d]     server_pre_key: %s" % ( self.session_id, debug.tohex ( server_pre_key ) ) )
			else:
				resp.append ( '' )

		#send response
		self._send_response ( RESP_HANDSHAKE_1, resp )

		if is_encryption_supported and self._encrypt_alg:

			debug.write ( 3, "[%d] _handshake(), reading command CMD_HANDSHAKE_2:" % self.session_id )
			
			#get handshake phase 2 command
			ret = self._get_command()
			if not ret:
				debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_2" % self.session_id )
				return 0

			cmd, params = ret

			if cmd != CMD_HANDSHAKE_2:
				debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_2, got command: %d" % ( self.session_id, cmd ) )
				return 0

			if len ( params ) < 1:
				debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_2, received params len: %d (expected at least: 1)" % ( self.session_id,
					len ( params ) ) )
				return 0
			if not isinstance ( params[0], str ):
				debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_2, param0 is %s (expected 'str')" % ( self.session_id,
					type(params[0]).__name__ ) )
				return 0
			block02 = self._server_rsa_key.decrypt ( params[0] )
			client_pre_key = read_pkcs_block_02 ( block02, RSA_KEY_LENGTH )
			if not client_pre_key:
				debug.error ( "[%d] _handshake(): error receiving CMD_HANDSHAKE_2, empty client_pre_key" % self.session_id )
				return 0

			debug.write ( 3, "[%d]     client_pre_key: %s" % ( self.session_id, debug.tohex ( client_pre_key ) ) )

			#derive other data
			debug.write ( 3, "[%d] handshake phase 2 derived data" % self.session_id )

			hmac_master_key = md5 ( self._client_random + self._server_random + client_pre_key ).digest()
			hmac_master_key += sha ( self._server_random + self._client_random + server_pre_key ).digest()

			debug.write ( 3, "[%d]     hmac_master_key: %s" % ( self.session_id, debug.tohex ( hmac_master_key ) ) )

			random_data = CTP_PRF ( hmac_master_key, "MASTER" + self._server_random + self._client_random, 32 + 2 * self._encrypt_alg.block_size )

			debug.write ( 3, "[%d]     random_data: %s" % ( self.session_id, debug.tohex ( random_data ) ) )

			client_init_vector = random_data[:self._encrypt_alg.block_size]
			server_init_vector = random_data[self._encrypt_alg.block_size:2 * self._encrypt_alg.block_size]
			self._hmac_key = random_data[2 * self._encrypt_alg.block_size:]

			debug.write ( 3, "[%d]     client_init_vector: %s" % ( self.session_id, debug.tohex ( client_init_vector ) ) )
			debug.write ( 3, "[%d]     server_init_vector: %s" % ( self.session_id, debug.tohex ( server_init_vector ) ) )
			debug.write ( 3, "[%d]     hmac_key: %s" % ( self.session_id, debug.tohex ( self._hmac_key ) ) )

			client_key = CTP_PRF ( hmac_master_key + client_pre_key, "CLIENT" + self._client_random + self._server_random, self._encrypt_alg.key_size )
			server_key = CTP_PRF ( hmac_master_key + server_pre_key, "SERVER" + self._client_random + self._server_random, self._encrypt_alg.key_size )

			debug.write ( 3, "[%d]     client_key: %s" % ( self.session_id, debug.tohex ( client_key ) ) )
			debug.write ( 3, "[%d]     server_key: %s" % ( self.session_id, debug.tohex ( server_key ) ) )

			self._client_sess_key = self._encrypt_alg.new ( client_key, self._encrypt_alg.MODE_CBC, client_init_vector )
			self._server_sess_key = self._encrypt_alg.new ( server_key, self._encrypt_alg.MODE_CBC, server_init_vector )

			#send response
			debug.write ( 3, "[%d] _handshake(), sending RESP_HANDSHAKE_2:" % self.session_id )

			resp = hmac.new ( self._hmac_key, client_pre_key + server_pre_key, md5 ).digest()
			resp += hmac.new ( self._hmac_key, server_pre_key + client_pre_key, sha ).digest()
			
			debug.write ( 3, "[%d]     mac verification data: %s" % ( self.session_id, debug.tohex ( resp ) ) )
			
			self._send_response ( RESP_HANDSHAKE_2, ( resp, ) )

			#activate encryption/compression settings
			self._handshake_complete = 1

			debug.write ( 2, "[%d] handshake completed successfully" % self.session_id )

		return 1

		
	# *******************************************************
	# _authenticate() - autentica la connessione
	def _authenticate ( self ):
		"""
		_authenticate ( self ) -> bool

		Effettua l'autenticazione dell'utente e della password.

		Riceve un comando CMD_LOGIN con argomenti login e password.

		Puo' inviare come risposta RESP_OK se l'autenticazione ha successo,
		altrimenti RESP_BADLOGIN.
		Se invia RESP_OK viene spedito un argomenti contenente una tupla dei
		metodi registrati sul server.
		"""

		debug.write ( 2, "[%d] entering _authenticate(), receiving command..." % self.session_id )
		
		ret = self._get_command()
		if not ret:
			debug.error ( "[%d] _authenticate(): error receiving CMD_LOGIN" % self.session_id )
			return 0

		cmd, params = ret

		if cmd != CMD_LOGIN:
			debug.error ( "[%d] _authenticate(): error receiving CMD_LOGIN, got command: %d" % ( self.session_id, cmd ) )
			return 0

		if len ( params ) != 2:
			debug.error ( "[%d] _authenticate(): error receiving CMD_LOGIN, received params len: %d (expected at least: 2)" % ( self.session_id,
				len ( params ) ) )
			return 0
		if not isinstance ( params[0], str ) or not isinstance ( params[1], str ):
			debug.error ( ( "[%d] _authenticate(): error receiving CMD_LOGIN:\n" +
					"       param0 is %s (expected 'str')\n" +
					"       param1 is %s (expected 'str')" ) %
					( self.session_id, type(params[0]).__name__, type(params[1]).__name__ )
				)
			return 0

		if self._ctppasswd:
			login, passwd = params
			
			login = login.lower()
			passwd = md5 ( passwd ).hexdigest()

			auth_ok = 0
			try:
				pwd = file ( self._ctppasswd )
				for l in pwd:
					l = l.strip()
					if not l or l[0] == '#': continue
					l = l.split ( ':' )
					if l[0] == login and l[1] == passwd:
						self.login = login
						auth_ok = 1
						break
			except:
				pass
		else:
			auth_ok = 1

		self._tc.lock_acquire ( "events_lock" )
		try:
			event_names = self._events.keys()
		finally:
			self._tc.lock_release ( "events_lock" )

		if auth_ok:
			debug.write ( 2, "[%d] _authenticate(): sending RESP_OK:" % self.session_id )
			debug.write ( 2, "[%d]     methods: %s" % ( self.session_id, self._methods.keys() ) )
			debug.write ( 2, "[%d]     events: %s" % ( self.session_id, event_names ) )
			self._send_response ( RESP_OK, ( self._methods.keys(), event_names ) )
		else:
			debug.write ( 2, "[%d] _authenticate(): sending RESP_BADLOGIN" % self.session_id )
			self._send_response ( RESP_BADLOGIN )
		
		return 1


# *******************************************************
# CTPRPCServer class
class CTPRPCServer ( object ):
	def __init__ ( self, listen_addr = None, ctppasswd_file = None, serialize_calls = 0, socket_timeout = 300 ):
		"""
		La class CTPRPCServer puo' essere inizializzata passando come parametro
		una tupla ( ip_address, port ) che viene utilizzata come indirizzo ip e porta
		di ascolto del server.
		"""
		
		super ( CTPRPCServer, self ).__init__()

		if not listen_addr: listen_addr = ( '', 0 )

		self._listen_addr = listen_addr
		self._instance = None
		self._class = None
		self._methods = {}
		self._events = {}
		self._ctppasswd = ctppasswd_file
		self._serialize_calls = serialize_calls
		self._socket_timeout = socket_timeout

		self._conn_count = 0
		self._keep_running = 1
		
		#creo la classe di gestione dei thread
		self._tc = os3.utils.thread_control.ThreadControl()
		
		#creo il socket principale
		self._main_sock = socket.socket ( socket.AF_INET, socket.SOCK_STREAM, 0 )
		self._main_sock.setsockopt ( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
		self._main_sock.bind ( self._listen_addr )
		self._main_sock.listen ( 10 )

		self._listen_addr = self._main_sock.getsockname()
		self._connection_list = []

		debug.write ( 1, "CTPRPCServer created (listen_addr: %s, ctppasswd: %s, serialize_calls: %d, socket_timeout: %d)" %
				( listen_addr, ctppasswd_file, serialize_calls, socket_timeout ) );
		

	# *******************************************************
	# _generate_rsa_keys() - rigenera le chiavi rsa
	def _generate_rsa_keys ( self ):
		"""
		_generate_rsa_keys ( self )

		Genera le chiavi RSA sel server.
		"""
		self._server_rsa_key = RSA.generate ( RSA_KEY_LENGTH, RANDPOOL.get_bytes )


	# *******************************************************
	# server_forever() - inizia un ciclo di ascolto delle connessioni
	def serve_forever ( self ):
		"""
		serve_forever ( self )

		Inizia un ciclo che si pone in ascolto di tutte le nuove connessioni.
		"""

		debug.write ( 1, "serve_forever() called" )
		
		#ciclo infinito
		while self._keep_running:
			self.handle_request()

		try: self._main_sock.close()
		except: pass


	# *******************************************************
	# handle_request() - gestisce una singola connessione
	def handle_request ( self ):
		"""
		handle_request ( self )

		Gestisce una singola nuova connessione.
		"""

		debug.write ( 1, "handle_request() called" )
		
		rl, wl, xl = select.select ( [ self._main_sock ], [], [ self._main_sock ] )
		if xl:
			debug.error ( "handle_request() failed: select() returned exception" )
			self._keep_running = 0
			return

		try:
			debug.write ( 2, "calling accept()" )
			client_sock, addr = self._main_sock.accept()
		except Exception, msg:
			debug.error ( "handle_request() failed: accept() returned exception %s" % msg )
			self._keep_running = 0
			return
			
		if not self._keep_running:
			self._keep_running = 0
			return

		saddr = str ( addr )
		debug.write ( 2, "accept()ed connection from %s" % str ( saddr ) )

		if is_encryption_supported:
			if self._conn_count % 10 == 0:
				debug.write ( 2, "generating new server rsa keys" )
				self._generate_rsa_keys()
				self._conn_count = 1
			else:
				self._conn_count += 1
			server_rsa_key = self._server_rsa_key
		else:
			server_rsa_key = None
		
		self._connection_list = [ conn for conn in self._connection_list if self._tc.is_still_running ( conn[0] ) ]
		self._connection_list.append ( ( saddr, client_sock ) )

		debug.write ( 2, "spawning new connection thread" )
		self._tc.spawn ( saddr, self._handle_connection, ( saddr, client_sock, server_rsa_key, self._ctppasswd ) )

	
	# *******************************************************
	# stop_serving() - termina l'ascolto di nuove connessioni
	def stop_serving ( self ):
		"""
		stop_serving ( self )

		Termina l'ascolto di nuove connessioni
		"""

		debug.write ( 1, "stop_serving() called" )
		
		self._keep_running = 0
		try: self._main_sock.close()
		except: pass


	# *******************************************************
	# destroy_connections () - distrugge le connessioni correnti
	def destroy_connections ( self ):
		"""
		destroy_connection ( self )

		Termina le connessioni correnti
		"""

		debug.write ( 1, "destroy_connections() called" )

		for conn in self._connection_list:
			if not self._tc.is_still_running ( conn[0] ): continue
			conn[1].close()
		
		
	# *******************************************************
	# register_class() - pubblica una classe; crea un oggetto per ogni nuova nonnessione
	def register_class ( self, obj, init_params = None, init_params_dict = None ):
		"""
		register_class ( self, obj, init_params, init_params_dict )

		Registra una nuova classe che verra' istanziata per ogni nuova connessione.
		Il server pubblichera' al client i metodi dell'istanza della classe creata.

		Se l'istanza della classe possiede un metodo chiamato "list_methods()" allora
		verra' invocato per recuperare la lista dei metodi supportati.

		All'instanza verra' automaticamente settato una variaible membro chiamata "_ctp_connection"
		che e' un handle al relativo oggetto CTPRPCConnection da usare per recuperare parametri
		quali indirizzi ip e algoritmi di cifratura utilizzati.

		La classe puo' venire istanziata con alcuni parametri passati al costrutture:
		``init_params`` contiene una tupla di parametri posizionali mentre ``init_params_dict``
		contiene un dizionario di parametri con nome.
		"""

		debug.write ( 1, "register_class() called: class_name=%s" % obj.__name__ )
		
		if not init_params: init_params = ()
		if not init_params_dict: init_params_dict = {}
		
		self._methods = {}
		self._instance = None
		self._class = obj
		self._init_params = init_params
		self._init_params_dict = init_params_dict


	# *******************************************************
	# register_instance() - pubblica un'istanza di un oggetto
	def register_instance ( self, obj ):
		"""
		register_instance ( self, obj )

		Registra un oggetto gia' istanziato che verra' utilizzato per ogni nuova connessione.
		Il server pubblichera' al client i metodi di tale istanza.

		All'instanza verra' automaticamente settato una variaible membro chiamata "_ctp_connection"
		che e' un handle al relativo oggetto CTPRPCConnection da usare per recuperare parametri
		quali indirizzi ip e algoritmi di cifratura utilizzati.

		Se l'istanza possiede un metodo chiamato "list_methods()" allora
		verra' invocato per recuperare la lista dei metodi supportati.
		"""
		
		debug.write ( 1, "register_instance() called: obj=%s" % obj )
		
		self._class = None
		self._methods = {}
		self._instance = obj

	
	# *******************************************************
	# register_function() - pubblica una nuova funzione
	def register_function ( self, func, func_name = None ):
		"""
		register_function ( self, func, func_name )

		Registra un nuovo metodo che il server pubblichera' ai client.

		Il metodo puo' anche essere pubblicato con un nome diverso specificato
		in ``func_name``.
		"""

		debug.write ( 1, "register_function() called: func=%s" % func )
		
		self._class = None
		self._instance = None
		
		if not func_name: func_name = func.func_name
		self._methods[func_name] = func


	# *******************************************************
	# register_event() - pubblica un nuovo evento
	def register_event ( self, event_name ):
		"""
		register_event ( self, event_name )

		Registra un nuovo evento che il server pubblichera' ai client.
		"""

		debug.write ( 1, "register_event() called: event_name=%s" % event_name )
		
		self._tc.lock_acquire ( "events_lock" )
		try:
			if self._events.has_key ( event_name ): return
			self._events [ event_name ] = []
		finally:
			self._tc.lock_release ( "events_lock" )


	# *******************************************************
	# fire_event() - genera un evento
	def fire_event ( self, event_name, *args, **kwargs ):
		"""
		fire_event ( self, event_name, *args, **kwargs )

		Genera un evento verso i client sottoscritti
		"""

		debug.write ( 1, "fire_event() called: event_name=%s" % event_name )

		self._tc.lock_acquire ( "events_lock" )
		try:
			clients = self._events [ event_name ] [ : ]
		finally:
			self._tc.lock_release ( "events_lock" )
			
		for conn in clients:
			conn.fire_event ( event_name, *args, **kwargs )
	
	
	# *******************************************************
	# _handle_connection() - gestisce una nuova connessione
	def _handle_connection ( self, thread_name, client_sock, server_rsa_key, ctppasswd ):
		"""
		Questa funzione gestisce una nuova connessione creando un relativo oggetto
		CTPRPCConnection.
		"""

		debug.write ( 2, "_handle_connection() invoked in a new thread" )

		instance = None
		methods = {}
		
		if self._class:
			#create instance
			debug.write ( 2, "_handle_connection(): creating class instance" )
			try: instance = self._class ( *self._init_params, **self._init_params_dict )
			except Exception, msg:
				debug.error ( "_handle_connection(): exception while creating class instance (%s)" % msg )
				return 0
		elif self._instance:
			instance = self._instance
		elif self._methods:
			methods = self._methods

		if instance:
			try:
				debug.write ( 2, "_handle_connection(): trying to get instance method list" )
				method_list = instance.list_methods ()
			except AttributeError:
				method_list = [ item for item in dir ( instance )
					if not item.startswith ( "_" ) and callable ( getattr ( instance, item ) ) ]

			if method_list:
				for meth in method_list:
					methods[meth] = getattr ( instance, meth )
			
		if not methods:
			debug.error ( "_handle_connection(): no published method found, aborting connection" )
			return 0

		debug.write ( 2, "_handle_connection(): creating new CTPRPCConnection object" )
		conn = CTPRPCConnection ( self._tc, thread_name, client_sock, methods, self._events, instance, server_rsa_key,
			ctppasswd, self._serialize_calls, self._socket_timeout )

		if self._class and instance:
			instance._ctp_connection = conn
			try: instance.create()
			except: pass
		
		try:
			try:
				debug.write ( 2, "_handle_connection(): handling new connection" )
				conn.handle()

			except Exception, msg:
				debug.error ( "_handle_connection(): exception caught, quitting connection (%s)" % msg )

		finally:

			debug.write ( 2, "_handle_connection(): closing connection" )

			try: client_sock.close()
			except: pass

			if instance:
				debug.write ( 2, "_handle_connection(): destroying class instance" )
				try: instance.destroy_connection ( conn )
				except: pass
			
			if self._class and instance:
				try: instance.destroy()
				except: pass
				instance = None

			conn.close()


######################################################
###                    TEST AREA                   ###
######################################################
if __name__ == '__main__':
	class test_class ( object ):
		def test ( self, s, f ):

			#i dati della connessione sono in self._ctp_connection
			print "encrypt_alg: ", self._ctp_connection.encrypt_alg_id
			print "compress_alg:", self._ctp_connection.compress_alg_id
			print "client_addr: ", self._ctp_connection.client_address
			print "server_addr: ", self._ctp_connection.server_address
			print "client_language: ", self._ctp_connection.client_language

			serv.fire_event ( "test", s, f )

			return s, f

	debug.level = 100

	serv = CTPRPCServer ( ( "", 8000 ) )
	serv.register_class ( test_class )
	serv.register_event ( "test" )
	serv.serve_forever()

